<html>

<head>
    <meta charset="utf-8">
    <title>OLED 屏幕可视化设计</title>
    <script src="./vue.min.js"></script>
    <link rel="stylesheet" href="./style.css">
    </link>
    <style>
        body {
            padding: 0;
            margin: 0;
        }

        .main-container {
            width: 100%;
            min-width: 1500px;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            gap: 20px;
        }

        .analog-oled-container {
            width: 1407px;
            height: 703px;
            min-height: 703px;
            min-width: 1407px;
            display: grid;
            grid-template-columns: repeat(128, 9px);
            grid-template-rows: repeat(64, 9px);
            gap: 2px;
            background-color: #29154a;
            border: #000 15px solid;
        }

        .analog-single-pixel:hover {
            scale: calc(1.1);
            border: #fff 1px solid;
            /* cursor: pointer; */
        }

        .reality-oled-container {
            width: 510px;
            height: 254px;
            min-width: 510px;
            min-height: 254px;
            position: absolute;
            bottom: -493px;
            display: grid;
            grid-template-columns: repeat(128, 2px);
            grid-template-rows: repeat(64, 2px);
            gap: 2px;
            background-color: #29154a;
        }

        .highlighted-row {
            border: 1px solid #ffcc00 !important;
            /* 金色边框高亮 */
            box-shadow: 0 0 5px rgba(255, 204, 0, 0.7);
            /* 添加发光效果 */
        }

        .import-box {
            width: 500px;
            height: 500px;
            background-color: #fff;
            border: gold 5px solid;
            position: absolute;
            top: 10px;
            z-index: 3000;
            display: flex;
            flex-direction: column;
        }

        .close-btn {
            position: absolute;
            top: 5px;
            right: 5px;
            font-size: 20px;
        }

        .close-btn:hover {
            cursor: pointer;
            color: #5682e8;
        }

        .font-preview {
            margin-top: 5px;
            padding: 8px;
            background-color: #f8f8f8;
            border: 1px solid #ddd;
            border-radius: 3px;
            font-size: 16px;
            line-height: 1.5;
            min-height: 50px;
        }
    </style>
</head>

<body>
    <div class="main-container">
        <div>
            <div class="analog-oled-container" @mousedown="startDraw" @mousemove="continueDraw" @mouseup="endDraw"
                @mouseleave="endDraw" @contextmenu.prevent>
                <div class="analog-single-pixel"
                    :class="{ 'highlighted-row': highlightedRows.includes(Math.floor(index / 128)) | highlightedCols.includes(index % 128) }"
                    v-for="(pixel, index) in pixels" :key="index" :style="{backgroundColor: pixel ? '#fff' : '#54575c'}"
                    @click="togglePixel(index)">
                </div>
            </div>
            <div style="margin-top: 10px; display: flex; align-items: center; gap: 5px;">
                <p style="width: 140px; margin: 0px;">坐标：x: {{x}}, y: {{y}}</p>
                <p>page: {{page}}</p>
                <button @click="clearAll()">清空所有</button>
                <button @click="copyCoordinates()">拷贝坐标</button>
                <button @click="importCoordinates()">导入坐标</button>
                <button @click="copyAsPageCol()">拷贝为页/列地址</button>
                <button @click="copyAsChacheArr()">拷贝为缓存数组</button>
                <button @click="openGenerateFontBox()">生成字模</button>
                <button @click="openImportFontBox()">导入字模</button>
            </div>
            <div style="display: flex; align-items: center; gap: 5px;">
                <p>绘制模式: </p>
                <label class="switch">
                    <input type="checkbox" @click="toggleDrawMode()">
                    <span class="slider round"></span>
                </label>
                <p>页边界高亮显示: </p>
                <label class="switch">
                    <input type="checkbox" @click="togglePageEdgeHighlight()">
                    <span class="slider round"></span>
                </label>
                <p>垂直中线高亮显示: </p>
                <label class="switch">
                    <input type="checkbox" @click="toggleCenterVLineHighlight()">
                    <span class="slider round"></span>
                </label>
                <p>水平中线高亮显示: </p>
                <label class="switch">
                    <input type="checkbox" @click="toggleCenterHLineHighlight()">
                    <span class="slider round"></span>
                </label>
            </div>
            <div style="display: flex; gap: 5px; flex-direction: column;">
                <div style="display: flex; align-items: center; gap: 5px;">
                    区域操作:
                    <label>左上点:</label>
                    X:<input type="number" v-model="rsx" style="width: 50px;">
                    </input>
                    Y:<input type="number" v-model="rsy" style="width: 50px;">
                    </input>
                    <label>右下点:</label>
                    X:<input type="number" v-model="rex" style="width: 50px;">
                    </input>
                    Y:<input type="number" v-model="rey" style="width: 50px;">
                    </input>
                </div>
                <div style="margin-left: 30px; display: flex; gap: 5px;">
                    <li>区域移动:</li>
                    上:<input type="number" v-model="rmUp" style="width: 50px;">
                    </input>
                    右:<input type="number" v-model="rmRight" style="width: 50px;">
                    </input>
                    下:<input type="number" v-model="rmDown" style="width: 50px;">
                    </input>
                    左:<input type="number" v-model="rmLeft" style="width: 50px;">
                    </input>
                    <button @click="moveRegion()">移动</button>
                </div>
                <div style="margin-left: 30px; display: flex; gap: 5px;">
                    <li>区域清除:</li>
                    <button @click="clearRegion()">清除</button>
                </div>
                <div style="margin-left: 30px; display: flex; gap: 5px;">
                    <li>区域反显:</li>
                    <button @click="toggleRegion()">反显</button>
                </div>
            </div>
        </div>
        <div class="reality-oled-container">
            <div class="reality-single-pixel" v-for="(pixel, index) in pixels" :key="index"
                :style="{backgroundColor: pixel ? '#fff' : '#54575c'}" @click="togglePixel(index)">
            </div>
        </div>
        <div>
            <img src="./img/true.png" width="540px" height="490px" style="margin-left: 2px;">
        </div>
        <div v-if="importBoxVisible" class="import-box">
            <div @click="closeImportBox()" class="close-btn">X</div>
            <textarea style="width: 100%; height: 80%;" v-model="importArrStr"></textarea>
            <button style="width: 100px; height: 30px;" @click="importBox(true)">覆盖式导入</button>
            <button style="width: 100px; height: 30px;" @click="importBox(false)">非覆盖式导入</button>
        </div>
        <div v-if="importFontBoxVisible" class="import-box">
            <div @click="closeImportFontBox()" class="close-btn">X</div>
            <textarea style="width: 100%; height: 80%;" v-model="importFontData"></textarea>
            <div style="display: flex; flex-direction: column; gap: 5px;">
                <div>
                    <code style="color: red;">*各个字模间以英文分号';'分隔，字模内以英文逗号','分隔</code>
                </div>
                <div>
                    <label>起始点:</label>
                    Page(0~7):<input type="number" v-model.number="fontImportSP" style="width: 50px;">
                    </input>
                    Column(~127):<input type="number" v-model.number="fontImportSC" style="width: 50px;">
                    </input>
                </div>
                <div>
                    字宽:<input type="number" v-model.number="fontW" style="width: 50px;">
                    </input>
                    字高:<input type="number" v-model.number="fontH" style="width: 50px;">
                    </input>
                </div>
                <button style="width: 100px; height: 30px;" @click="importFont()">导入</button>
            </div>
        </div>
        <div v-if="generateFontBoxVisible" class="import-box">
            <div @click="closeGenerateFontBox()" class="close-btn">X</div>
            <textarea style="width: 100%; height: 50%;" v-model="generateFontText"></textarea>
            <div style="display: flex; flex-direction: column; gap: 5px;">
                <div>
                    <label>起始点:</label>
                    Page(0~7):<input type="number" v-model.number="generateFontSP" style="width: 50px;">
                    </input>
                    Column(0~127):<input type="number" v-model.number="generateFontSC" style="width: 50px;">
                    </input>
                </div>
                <div>
                    字宽:<input type="number" v-model.number="generateFontW" style="width: 50px;">
                    </input>
                    字高:<input type="number" v-model.number="generateFontH" style="width: 50px;">
                    </input>
                </div>
                <div>
                    字体:
                    <select v-model="generateFontFamily" style="width: 150px;">
                        <option value="SimSun">宋体</option>
                        <option value="Microsoft YaHei">微软雅黑</option>
                        <option value="Arial">Arial</option>
                        <option value="Helvetica">Helvetica</option>
                        <option value="sans-serif">无衬线体</option>
                        <option value="monospace">等宽字体</option>
                    </select>
                </div>
                <div>
                    字体预览:
                    <div class="font-preview" :style="{fontFamily: generateFontFamily}">
                        中文：你好世界<br>
                        英文：Hello World
                    </div>
                </div>
                <div>
                    <button style="width: 100px; height: 30px;" @click="generateFont()">生成</button>
                    <button style="width: 100px; height: 30px;" @click="copyFont()">复制</button>
                </div>
            </div>
        </div>
        <img src="./img/pic1.png" style="width: 1200px;">
    </div>
</body>

<script>
    new Vue({
        el: '.main-container',
        data: {
            pixels: Array(8192).fill(false),
            x: 0,
            y: 0,
            page: 0,
            importArrStr: "",
            importBoxVisible: false,
            pageColAdd: {
                page0: [],
                page1: [],
                page2: [],
                page3: [],
                page4: [],
                page5: [],
                page6: [],
                page7: [],
            },
            isDrawing: false,
            isErasing: false,
            drawMode: false,
            // 缓存容器的位置信息，但浏览器窗口发生改变或者滚动条时会出现错误
            containerRect: null,
            // =====区域移动相关=====
            rsx: 0,
            rsy: 0,
            rex: 0,
            rey: 0,
            rmUp: 0,
            rmDown: 0,
            rmLeft: 0,
            rmRight: 0,
            // =====页边界高亮=====
            highlightedRows: [],
            highlightedCols: [],
            // =====导入字模=====
            importFontBoxVisible: false,
            fontH: 14,
            fontW: 14,
            fontImportSP: 0,
            fontImportSC: 0,
            importFontData: "",
            // =====生成字模=====
            generateFontBoxVisible: false,
            generateFontSP: 0,
            generateFontSC: 0,
            generateFontH: 14,
            generateFontW: 14,
            generateFontText: "",
            generateFontFamily: "SimSun",
            // 缓存Canvas避免每次创建
            fontCanvas: null,
            fontCtx: null,
        },
        mounted() {
            // 监听滚动条和窗口尺寸改变，就更新containerRect
            window.addEventListener('scroll', this.clearContainerRectCache, true); // 捕获阶段更及时
            window.addEventListener('resize', this.clearContainerRectCache);
            this.fontCanvas = document.createElement('canvas');
            this.fontCtx = this.fontCanvas.getContext('2d');
        },
        beforeUnmount() {
            window.removeEventListener('scroll', this.clearContainerRectCache, true);
            window.removeEventListener('resize', this.clearContainerRectCache);
        },
        methods: {
            toggleDrawMode() {
                this.drawMode = !this.drawMode
            },
            startDraw(e) {
                if (!this.drawMode) {
                    return;
                }
                if (e.button === 0) {
                    this.isDrawing = true;
                    this.isErasing = false;
                } else if (e.button === 2) {
                    this.isErasing = true;
                    this.isDrawing = false;
                }
                e.preventDefault();
            },

            continueDraw(e) {
                this.calculatePixelCoordinates(e);
                if (this.isDrawing) {
                    this.onPixelAt(this.x, this.y);
                } else if (this.isErasing) {
                    this.offPixelAt(this.x, this.y);
                }
            },

            endDraw() {
                this.isDrawing = false;
                this.isErasing = false;
            },

            // 清空容器位置信息
            clearContainerRectCache() {
                this.containerRect = null;
            },

            calculatePixelCoordinates(e) {
                // 滚动页面或者改变窗口大小时，需要重新计算容器的位置信息
                if (!this.containerRect) {
                    const container = e.target.closest('.analog-oled-container');
                    this.containerRect = container.getBoundingClientRect();
                }
                const x = Math.floor((e.clientX - this.containerRect.left - 15) / 11);
                const y = Math.floor((e.clientY - this.containerRect.top - 15) / 11);
                if (x < 0 || x > 127 || y < 0 || y > 63) {
                    return;
                }
                this.x = x;
                this.y = y;
                this.page = Math.floor(y / 8);
            },

            onPixelAt(x, y) {
                const index = y * 128 + x;
                // 仅当状态变化时才更新
                this.$set(this.pixels, index, true);
            },

            offPixelAt(x, y) {
                const index = y * 128 + x;
                // 仅当状态变化时才更新
                this.$set(this.pixels, index, false);
            },

            /*=== 分割线上部分是绘制操作 ===*/

            togglePixel(index) {
                this.$set(this.pixels, index, !this.pixels[index]);
            },
            onPixel(index) {
                this.pixels[index] = true;
            },
            offPixel(index) {
                this.pixels[index] = false;
            },
            clearAll() {
                this.pixels = Array(8192).fill(false);
            },
            copyCoordinates() {
                let arr = [];
                for (let i = 0; i < this.pixels.length; i++) {
                    if (this.pixels[i]) {
                        arr.push(i);
                    }
                }
                if (!arr.length) {
                    alert('无像素拷贝');
                    return;
                }
                let outputArr = [];
                for (let i = 0; i < arr.length; i++) {
                    outputArr.push(`{${arr[i] % 128},${Math.floor(arr[i] / 128)}}`);
                }
                navigator.clipboard.writeText(outputArr.join(','));
                alert('已复制到剪贴板');
            },
            copyAsPageCol() {
                let arr = [];
                for (let i = 0; i < this.pixels.length; i++) {
                    if (this.pixels[i]) {
                        arr.push(i);
                    }
                }
                if (!arr.length) {
                    alert('无像素拷贝');
                    return;
                }
                Object.keys(this.pageColAdd).forEach(key => {
                    this.pageColAdd[key] = [];
                });
                for (let i = 0; i < arr.length; i++) {
                    const y = Math.floor(arr[i] / 128);
                    const x = arr[i] % 128;
                    const page = Math.floor(y / 8);
                    switch (page) {
                        case 0:
                            this.pageColAdd.page0.push([x, y]);
                            break;
                        case 1:
                            this.pageColAdd.page1.push([x, y]);
                            break;
                        case 2:
                            this.pageColAdd.page2.push([x, y]);
                            break;
                        case 3:
                            this.pageColAdd.page3.push([x, y]);
                            break;
                        case 4:
                            this.pageColAdd.page4.push([x, y]);
                            break;
                        case 5:
                            this.pageColAdd.page5.push([x, y]);
                            break;
                        case 6:
                            this.pageColAdd.page6.push([x, y]);
                            break;
                        case 7:
                            this.pageColAdd.page7.push([x, y]);
                            break;
                    }
                }
                // 对每个页面中的坐标按x值分组
                Object.keys(this.pageColAdd).forEach(pageKey => {
                    const coords = this.pageColAdd[pageKey];
                    if (coords.length > 0) {
                        // 按x坐标排序
                        coords.sort((a, b) => a[0] - b[0]);

                        // 按相同x值分组
                        const groupedCoords = [];
                        let currentGroup = [coords[0]];

                        for (let i = 1; i < coords.length; i++) {
                            if (coords[i][0] === coords[i - 1][0]) {
                                currentGroup.push(coords[i]);
                            } else {
                                groupedCoords.push(currentGroup);
                                currentGroup = [coords[i]];
                            }
                        }
                        groupedCoords.push(currentGroup); // 添加最后一组

                        // 处理分组后的数据
                        groupedCoords.forEach((group, index) => {
                            let data = [0, 0, 0, 0, 0, 0, 0, 0];
                            let sx, sy;
                            let page = Math.floor(group[0][1] / 8);
                            sx = group[0][0];
                            sy = page * 8;
                            group.forEach(coord => {
                                let y = coord[1];
                                data[y - sy] = 1;
                            });
                            let hexValue = 0;
                            for (let i = 0; i < data.length; i++) {
                                hexValue |= (data[i] << i);
                            }
                            let hexString = '0x' + hexValue.toString(16).padStart(2, '0').toUpperCase();
                            hexString += `(${sx},${sy})`
                            groupedCoords[index] = hexString;
                        });
                        this.pageColAdd[pageKey] = groupedCoords;
                    }
                });
                // 将pageColAdd拷贝到剪贴板
                const finalData = JSON.stringify(this.pageColAdd);
                navigator.clipboard.writeText(finalData);
                alert('已复制到剪贴板');
            },
            // 拷贝为缓存数组格式
            copyAsChacheArr() {
                // 缓存数组cacheArr[8][128]
                let cacheArr = Array(8).fill().map(() => Array(128).fill(0));
                for (let page = 0; page < 8; page++) {
                    for (let x = 0; x < 128; x++) {
                        let byte = 0;
                        let i = 0;
                        for (let y = page * 8; y < page * 8 + 8; y++, i++) {
                            let index = y * 128 + x;
                            if (this.pixels[index]) {
                                byte |= (1 << i);
                            }
                        }
                        cacheArr[page][x] = byte;
                    }
                }
                let cacheArrHex = cacheArr.map(page => page.map(x => '0x' + x.toString(16).padStart(2, '0').toUpperCase()));
                cacheArrHex = cacheArrHex.map(page => '{' + page + '}').join(',\n');
                navigator.clipboard.writeText(cacheArrHex);
                alert('已复制到剪贴板');
            },

            importCoordinates() {
                this.importBoxVisible = true;
            },
            closeImportBox() {
                this.importBoxVisible = false;
            },
            importBox(val) {
                // 去除空格和回车，并分割成坐标字符串数组
                let coordinateStrings = this.importArrStr.replace(/\s/g, '').split(/(?<=\}),(?=\{)/);
                if (coordinateStrings.length == 1 && coordinateStrings[0] == '') {
                    alert('无坐标导入');
                    return;
                }
                if (val) {
                    this.clearAll();
                }
                // 处理每个坐标字符串
                for (let i = 0; i < coordinateStrings.length; i++) {
                    const coordStr = coordinateStrings[i];
                    // 提取坐标值（去除大括号）
                    const match = coordStr.match(/\{(\d+),(\d+)\}/);
                    if (match) {
                        const x = parseInt(match[1]);
                        const y = parseInt(match[2]);
                        // 计算索引（注意：这里应该是 x*128 + y）
                        const index = y * 128 + x;
                        if (index >= 0 && index < 8192) {
                            this.onPixel(index);
                        }
                    }
                }
                // 关闭导入框
                this.closeImportBox();
                this.importArrStr = "";
            },
            // ===================== 区域移动操作 =====================
            checkX(x) {
                if (x < 0 || x > 127) {
                    alert('左上或右下X坐标错误');
                    return false;
                }
            },
            checkY(y) {
                if (y < 0 || y > 63) {
                    alert('左上或右下坐标错误');
                    return false;
                }
            },
            getMoveData() {
                const up = this.rmDown - this.rmUp;
                const left = this.rmRight - this.rmLeft;
                return [up, left];
            },
            getTrueStartandEndXY() {
                let sx, sy, ex, ey;
                sx = Math.min(this.rsx, this.rex);
                ex = Math.max(this.rsx, this.rex);
                sy = Math.min(this.rsy, this.rey);
                ey = Math.max(this.rsy, this.rey);
                return [sx, sy, ex, ey];
            },
            checkXYWithMoveData(coords, moveData) {
                // coords: {startX, startY, endX, endY}
                // moveData: [up, left]
                if (coords[0] + moveData[1] < 0 || coords[0] + moveData[1] > 127 ||
                    coords[2] + moveData[1] < 0 || coords[2] + moveData[1] > 127) {
                    return 1;
                } else if (coords[1] + moveData[0] < 0 || coords[1] + moveData[0] > 63 ||
                    coords[3] + moveData[0] < 0 || coords[3] + moveData[0] > 63) {
                    return 2;
                }
                return 0;
            },
            checkRegionMoveParams() {
                this.checkX(this.rsx);
                this.checkY(this.rsy);
                this.checkX(this.rex);
                this.checkY(this.rey);
                const moveData = this.getMoveData();
                const coords = this.getTrueStartandEndXY();
                const result = this.checkXYWithMoveData(coords, moveData);
                if (result != 0) {
                    if (result == 1) alert('X坐标移动后错误');
                    else if (result == 2) alert('Y坐标移动后错误');
                    return fasle;
                }
                return { moveData: moveData, coords: coords };
            },
            moveRegion() {
                const data = this.checkRegionMoveParams();
                if (data == false) return;
                // 使用临时像素组，避免频繁改变响应式数据，导致不必要的DOM更新
                // 如此以来只需最后更新一次DOM，速度大大提升
                let newPixels = [...this.pixels];
                let updatedPixels = [];
                for (let y = data.coords[1]; y <= data.coords[3]; y++) {
                    for (let x = data.coords[0]; x <= data.coords[2]; x++) {
                        const index = y * 128 + x;
                        if (this.pixels[index]) {
                            // 避免修改已经是移动后被点亮的像素
                            if (updatedPixels.indexOf(index) == -1) {
                                newPixels[index] = false;
                            }
                            const newIndex = (y + data.moveData[0]) * 128 + x + data.moveData[1];
                            newPixels[newIndex] = true;
                            updatedPixels.push(newIndex);
                        }
                    }
                }
                this.pixels = newPixels;
            },

            clearRegion() {
                const data = this.checkRegionMoveParams();
                if (data == false) return;
                let newPixels = [...this.pixels];
                for (let y = data.coords[1]; y <= data.coords[3]; y++) {
                    for (let x = data.coords[0]; x <= data.coords[2]; x++) {
                        const index = y * 128 + x;
                        newPixels[index] = false;
                    }
                }
                this.pixels = newPixels;
            },

            toggleRegion() {
                const data = this.checkRegionMoveParams();
                if (data == false) return;
                let newPixels = [...this.pixels];
                for (let y = data.coords[1]; y <= data.coords[3]; y++) {
                    for (let x = data.coords[0]; x <= data.coords[2]; x++) {
                        const index = y * 128 + x;
                        newPixels[index] = !this.pixels[index];
                    }
                }
                this.pixels = newPixels;
            },

            // =====页边界高亮=====
            togglePageEdgeHighlight() {
                if (this.highlightedRows.length == 0 || this.highlightedRows.length == 2) {
                    this.highlightedRows = [0, 8, 16, 24, 32, 40, 48, 56];
                } else {
                    this.highlightedRows = [];
                }
            },

            toggleCenterVLineHighlight() {
                if (this.highlightedCols.length == 0) {
                    this.highlightedCols = [63, 64];
                } else {
                    this.highlightedCols = [];
                }
            },

            toggleCenterHLineHighlight() {
                if (this.highlightedRows.length == 8) return;
                if (!this.highlightedRows.includes(31) && !this.highlightedRows.includes(32)) {
                    this.highlightedRows.push(32, 31);
                } else {
                    this.highlightedRows = this.highlightedRows.filter(item => item !== 32 && item !== 31);
                }
            },

            // =====导入字模=====
            openImportFontBox() {
                this.importFontBoxVisible = true;
            },
            closeImportFontBox() {
                this.importFontBoxVisible = false;
            },
            importFont() {
                /*
                    各个字模键以英文分号';'分隔，字模内以英文逗号','分隔
                */
                const h = this.fontH;
                const w = this.fontW;
                const startY = this.fontImportSP * 8;
                const startX = this.fontImportSC;
                const fontData = this.importFontData.replace(/\s/g, '');
                const fonts = fontData.split(';');
                // 字体占几页， h / 8 向上取整
                const pNum = Math.ceil(h / 8);
                const idxs = [...this.pixels];
                for (let i = 0; i < fonts.length; i++) {
                    nowX = startX + w * i;
                    if (nowX + w > 127) {
                        break;
                    }
                    const font = fonts[i];
                    const coords = font.split(',');
                    for (let i = 0; i < w; ++i) {
                        for (let j = 0; j < pNum; ++j) {
                            const byte = parseInt(coords[i + j * w], 16);
                            if (byte == 0) {
                                continue;
                            }
                            // 按照byte中为1的bit，设置像素
                            for (let k = 7; k >= 0; --k) {
                                const bit = (byte >> k) & 1;
                                if (bit) {
                                    const x = nowX + i;
                                    const y = startY + j * 8 + k;
                                    const index = y * 128 + x;
                                    idxs[index] = true;
                                }
                            }
                        }
                    }
                }
                this.pixels = idxs;
            },

            // =====生成字模=====
            openGenerateFontBox() {
                this.generateFontBoxVisible = true;
            },
            closeGenerateFontBox() {
                this.generateFontBoxVisible = false;
            },
            generateFont() {
                const text = this.generateFontText.replace(/\s/g, '');
                if (!text) {
                    alert('请输入字模数据');
                    return;
                }
                const w = this.generateFontW; // 字宽
                const h = this.generateFontH; // 字高
                const startY = this.generateFontSP * 8; // y: 0,8,16,...,56
                const allCoords = [];
                for (let i = 0; i < text.length; i++) {
                    const char = text[i];
                    const startX = this.generateFontSC + w * i; // x: 0 ~ 127
                    const coords = this.generateFontData(char, w, h, startX, startY);
                    if (coords.length == 0) {
                        break;
                    }
                    allCoords.push(...coords);
                }
                const newPixels = [...this.pixels];
                for (const [x, y] of allCoords) {
                    const index = y * 128 + x;
                    newPixels[index] = true;
                }
                this.pixels = newPixels;
            },

            generateFontData(char, w, h, startX, startY) {
                const coords = [];
                // 安全检查：如果字完全超出屏幕，直接返回空
                if (startX >= 128 || startY >= 64 || startX + w <= 0 || startY + h <= 0) {
                    return coords;
                }

                // 创建离屏 canvas 渲染单个字符
                this.fontCanvas.width = w;
                this.fontCanvas.height = h;
                const ctx = this.fontCtx;

                // 白底黑字（阳码：黑=1=点亮）
                ctx.fillStyle = '#ffffff';
                ctx.fillRect(0, 0, w, h);
                ctx.font = `${h}px "${this.generateFontFamily}", sans-serif`;
                ctx.textAlign = 'left';
                ctx.textBaseline = 'top'; // 关键：确保从 (0,0) 开始绘制
                ctx.fillStyle = '#000000';
                ctx.fillText(char, 0, 0);

                // 获取图像数据
                const imageData = ctx.getImageData(0, 0, w, h);
                const data = imageData.data; // RGBA 数组

                // 遍历每个像素 (dx, dy) in [0, w) x [0, h)
                for (let dy = 0; dy < h; dy++) {
                    for (let dx = 0; dx < w; dx++) {
                        const idx = (dy * w + dx) * 4;
                        const r = data[idx];
                        const g = data[idx + 1];
                        const b = data[idx + 2];
                        // 简单灰度判断：越暗越可能点亮
                        const gray = (r + g + b) / 3;
                        if (gray < 128) { // 黑色或深色 → 视为“点亮”
                            const x = startX + dx;
                            const y = startY + dy;
                            // 检查是否在 OLED 屏幕范围内 (x:0-127, y:0-63)
                            if (x >= 0 && x < 128 && y >= 0 && y < 64) {
                                coords.push([x, y]);
                            }
                        }
                    }
                }
                return coords;
            },

            copyFont() {
                const text = this.generateFontText.replace(/\s/g, '');
                if (!text) {
                    alert('请输入字模数据');
                    return;
                }
                const w = this.generateFontW; // 字宽
                const h = this.generateFontH; // 字高
                const startY = this.generateFontSP * 8; // y: 0,8,16,...,56

                // 计算字模占的页数（高度除以8向上取整）
                const pNum = Math.ceil(h / 8);

                // 用于存储最终的字模数据
                const fontDataArray = [];

                for (let i = 0; i < text.length; i++) {
                    const char = text[i];
                    const startX = this.generateFontSC + w * i; // x: 0 ~ 127
                    const coords = this.generateFontData(char, w, h, startX, startY);

                    // 创建一个二维数组来存储字模数据，[pNum][w]
                    const fontMatrix = Array.from({ length: pNum }, () => Array(w).fill(0));

                    // 将坐标转换为字模矩阵
                    for (const [x, y] of coords) {
                        // 计算相对坐标
                        const relX = x - startX;
                        const relY = y - startY;

                        // 检查是否在字模范围内
                        if (relX >= 0 && relX < w && relY >= 0 && relY < h) {
                            // 计算页索引和位索引
                            const pageIndex = Math.floor(relY / 8);
                            const bitIndex = relY % 8;

                            // 设置对应的位（MSB在前）
                            fontMatrix[pageIndex][relX] |= (1 << bitIndex);
                        }
                    }

                    // 将字模矩阵转换为字节数组字符串
                    const fontBytes = [];
                    for (let j = 0; j < pNum; j++) {
                        for (let k = 0; k < w; k++) {
                            fontBytes.push(`0x${fontMatrix[j][k].toString(16).padStart(2, '0').toUpperCase()}`);
                        }
                    }

                    // 将字模添加到结果数组
                    fontDataArray.push(fontBytes.join(','));
                }

                // 用分号连接所有字模
                const finalData = fontDataArray.join(';');

                // 复制到剪贴板
                navigator.clipboard.writeText(finalData).then(() => {
                    alert('字模数据已复制到剪贴板');
                }).catch(err => {
                    console.error('复制失败:', err);
                    alert('复制失败，请手动复制');
                });
            }
        }
    })
</script>

</html>